<html>
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
        <link rel="shortcut icon" href="">
        <title>Crash Test</title>
        <style type="text/css">
           body {
              background-color: #eccc68;
            }
            canvas {
              width: 400px;
              height: 300px;
              border: 1px solid black;
            }
        </style>
    </head>
    <body>
        <h1>Crash Test</h1>
        <script type="text/javascript">
            const WIDTH = 800, HEIGHT = 600
            const gravity = 980
            let canvas = document.createElement('canvas')
            canvas.width = WIDTH
            canvas.height = HEIGHT
            canvas.style.width = (WIDTH / 2) + "px"
            canvas.style.height = (HEIGHT / 2) + "px"
            canvas.style.border = "1px solid black"
            let ctx = canvas.getContext('2d')
            document.querySelector('body').appendChild(canvas)
            class Gameboard {
                constructor(cor = 0.9) {
                    this.startTime
                    this.tickLength = 20
                    this.lastTick = 0
                    this.init()
                    this.cor = cor
                }
                init() {
                    // ctx.circleId = 0
                    this.circles = [
                        new Circle(ctx, 30, 50, 30, -100, 390, 30),
                        new Circle(ctx, 60, 180, 20, 180, -275, 20),
                        new Circle(ctx, 120, 100, 60, 120, 262, 60),
                        new Circle(ctx, 150, 180, 10, -130, 138, 10),
                        new Circle(ctx, 190, 210, 40, 138, -280, 40),
                        new Circle(ctx, 220, 240, 50, 142, 350, 50),
                        new Circle(ctx, 120, 240, 70, 142, -320, 70),
                        new Circle(ctx, 120, 140, 80, -142, -320, 80),
                        new Circle(ctx, 420, 140, 90, -242, -320, 90),
                        new Circle(ctx, 420, 440, 100, -52, 120, 100),
                    ]
                    // requestAnimationFrame(this.process.bind(this));
                    this.process(performance.now())
                }
                process(tFrame) {
                    this.stopKey = requestAnimationFrame(this.process.bind(this))
                    let nextTick = this.lastTick + this.tickLength
                    let tickNum = 0
                    if (tFrame > nextTick) {
                        tickNum = Math.floor((tFrame - this.lastTick) / this.tickLength)
                    }

                    this.update (tickNum)
                    this.render ()
                    this.startTime = tFrame
                    // requestAnimationFrame(this.process.bind(this))
                }
                update(tickNum) {
                    for (let j = 0; j < tickNum; ++j) {
                        this.lastTick += this.tickLength
                        for (let i = 0; i < this.circles.length; i++) {
                            this.circles[i].fps = this.tickLength / 1000
                        }
                        this.checkCollision()
                        for (let i = 0; i < this.circles.length; i++) {
                            this.circles[i].update()
                        }
                    }
                }
                render () {
                    ctx.clearRect(0, 0, WIDTH, HEIGHT)
                    for (let i = 0; i < this.circles.length; i++) {
                        this.circles[i].draw(ctx)
                    }
                }

	            checkCollision() {
                    this.circles.forEach(circle => {
                        circle.colliding = false
                        let cor = this.cor
                        if (circle.x - circle.r <= 0) {
                            circle.vx *= cor * -1
                            circle.x = circle.r + 1
                            // circle.colliding = true
                        }
                        if (circle.x + circle.r >= WIDTH) {
                            circle.vx *= cor * -1
                            circle.x = WIDTH - circle.r - 1
                            // circle.colliding = true
                        }
                        if (circle.y - circle.r <= 0) {
                            circle.vy *= cor * -1
                            circle.y = circle.r + 1
                            // circle.colliding = true
                        }
                        if (circle.y + circle.r > HEIGHT) {
                            circle.vy *= cor * -1
                            circle.y = HEIGHT - circle.r
                            // circle.colliding = true
                        }
                        for (let i = 0, l = this.circles.length; i < l; ++i) {
                            let cur = this.circles[i]
                            if (circle.id == cur.id) continue
                            if (circle.isCircleCollided(cur)) {
                                circle.colliding = true
                                cur.colliding = true
                                circle.changeVelocityAndDirection(cur, cor)
                            }
                        }
                    })
                }
            }
            class Circle {
                constructor(context, x, y, r, vx, vy, mass=1) {
                    if (!context.circleId) context.circleId = 1
                    context.circleId += 1
                    this.id = context.circleId
                    this.context = context
                    this.x = x
                    this.y = y
                    this.r = r
                    this.vx = vx
                    this.vy = vy
                    this.colliding = false
                    this.mass = mass
                    this.fps = 0
                }
	            // 绘制小球
                draw() {
                    // this.context.fillStyle = "hsl(170, 100%, 50%)"
                    this.context.fillStyle = this.colliding ? "hsl(300, 100%, 70%)" : "hsl(170, 100%, 50%)"
                    this.context.strokeStyle = "hsl(220, 100%, 50%)"
                    // this.context.strokeStyle = this.colliding ? "hsl(120, 100%, 50%)" : "hsl(220, 100%, 50%)"
                    this.context.beginPath()
                    this.context.arc(this.x, this.y, this.r, 0, 2 * Math.PI)
                    this.context.fill()
                    this.context.stroke()
                }
                /**
               * 更新画布
               * @param {number} seconds
               */
                update() {
                    this.vy += gravity * this.fps
                    this.x += this.vx * this.fps;
                    this.y += this.vy * this.fps;
                }
                isCircleCollided(other) {
                    let distance = Math.sqrt((this.x - other.x) * (this.x - other.x) +
                      (this.y - other.y) * (this.y - other.y));
                    let radius = (this.r + other.r);
                    return distance <= radius;
                }
                changeVelocityAndDirection(other, cor) {
                    // 创建两小球的速度向量
                    let velocity1 = new Vector(this.vx, this.vy)
                    let velocity2 = new Vector(other.vx, other.vy)
                    // 小球1圆心指向小球2圆心连线方向的向量
                    let vNorm = new Vector(this.x - other.x, this.y - other.y)
                    // 小球1圆心指向小球2圆心连线方向的单位向量
                    let unitVNorm = vNorm.normalize()
                    // 碰撞点处的切线方向单位向量
                    let unitVTan = new Vector(-unitVNorm.y, unitVNorm.x)
                    // 小球1速度在连线方向的投影(标量)
                    let v1n = velocity1.dot(unitVNorm)
                    // 小球1速度在切线方向的投影(标量)
                    let v1t = velocity1.dot(unitVTan)
                    // 小球2速度在连线方向的投影(标量)
                    let v2n = velocity2.dot(unitVNorm)
                    // 小球2速度在切线方向的投影(标量)
                    let v2t = velocity2.dot(unitVTan)
                    // 连线方向的投影代入公式计算得到碰撞之后的速度值(标量)
                    // 完全弹性碰撞
                    /*
                    let v1nAfter = (v1n * (this.mass - other.mass) + 2 * other.mass * v2n) / (this.mass + other.mass)
	                let v2nAfter = (v2n * (other.mass - this.mass) + 2 * this.mass * v1n) / (this.mass + other.mass)
	                */
	                // 衰减系数
	                let v1nAfter = (this.mass * v1n + other.mass * v2n + cor * other.mass * (v2n - v1n)) / (this.mass + other.mass)
                    let v2nAfter = (this.mass * v1n + other.mass * v2n + cor * this.mass * (v1n - v2n)) / (this.mass + other.mass)
	                /*
	                */
	                if (v1nAfter < v2nAfter) return
                    // 连线方向的速度值乘以连线方向的单位向量得到碰撞后的连线方向速度分量
                    let v1VectorNorm = unitVNorm.multiply(v1nAfter)
                    let v2VectorNorm = unitVNorm.multiply(v2nAfter)
                    // 切线方法的速度值乘以切线方向的单位向量得到碰撞后切线方向速度分量
                    let minVy = 10, minVx = 0
                    // v1t = Math.abs(v1t) < minV ? 0 : v1t
                    // v2t = Math.abs(v2t) < minV ? 0 : v2t
                    let v1VectorTan = unitVTan.multiply(v1t)
                    let v2VectorTan = unitVTan.multiply(v2t)
                    // 小球1的速度向量
                    // v1VectorTan.y = v1VectorTan.y < minVy ? 0 : v1VectorTan.y
                    // v2VectorTan.y = v2VectorTan.y < minVy ? 0 : v2VectorTan.y

                    // v1VectorTan.x = v1VectorTan.x < minVx ? 0 : v1VectorTan.x
                    // v2VectorTan.x = v2VectorTan.x < minVx ? 0 : v2VectorTan.x
                    let velocity1After = v1VectorNorm.add(v1VectorTan)
                    let velocity2After = v2VectorNorm.add(v2VectorTan)
                    // 设置碰撞后的速度
                    this.vx = velocity1After.x
                    this.vy = velocity1After.y
                    other.vx = velocity2After.x
                    other.vy = velocity2After.y

                    let distance = Math.sqrt(Math.pow(this.x - other.x, 2) + Math.pow(this.y - other.y, 2));
                    let radius = this.r + other.r;
                    let gap = (radius - distance - 1) / 2
                    // console.log(radius - distance)
                    let unit1t2Norm = new Vector(this.x - other.x, this.y - other.y).normalize()
                    let tmp1 = new Vector(this.x, this.y).add(unit1t2Norm.multiply(gap))
                    this.x = tmp1.x
                    this.y = tmp1.y
                    let tmp2 = new Vector(other.x, other.y).add(unit1t2Norm.multiply(gap * -1))
                    other.x = tmp2.x
                    other.y = tmp2.y
                }
            }

            class Vector {
                constructor(x, y) {
                    this.x = x;
                    this.y = y;
                }
                /**
                 * 向量加法
                 * @param {Vector} v
                 */
                add(v) {
                    return new Vector(this.x + v.x, this.y + v.y);
                }
                /**
                 * 向量减法
                 * @param {Vector} v
                 */
                substract(v) {
                    return new Vector(this.x - v.x, this.y - v.y);
                }
                /**
                 * 向量与标量乘法
                 * @param {Vector} s
                 */
                multiply(s) {
                    return new Vector(this.x * s, this.y * s);
                }
                /**
                 * 向量与向量点乘（投影）
                 * @param {Vector} v
                 */
                dot(v) {
                    return this.x * v.x + this.y * v.y;
                }
                /**
                 * 向量标准化（除去长度）
                 * @param {number} distance
                 */
                normalize() {
                    let distance = Math.sqrt(this.x * this.x + this.y * this.y);
                    return new Vector(this.x / distance, this.y / distance);
                }
            }
            ;(function(){
                let gBoard = new Gameboard()
            })();

        </script>
    </body>
</html>
